﻿/// <copyright file="PppoeProtocolPacket.cs" company="not for commercial use">
/// Copyright (c) 6/4/2012 All Right Reserved
/// </copyright>
/// <author>Dimitry Kaplin</author>
/// <date>6/4/2012 21:35 </date>
/// <summary>PPPoE Protocol packet class... pretty ugly</summary>
using System;
using System.Collections.Generic;
using System.Text;
using EthernetTools.Utils;
using EthernetTools.DataBindinds;
using System.Windows.Forms;

namespace EthernetTools.Protocols.PPPoE
{
    public class PppoeProtocolPacket : IProtocol
    {
        #region Constant And Fields

        /// <summary>
        /// Delegate to log output function
        /// </summary>
        private EthernetTools.Utils.Delegates.HandleLogDelegate LogWriteDelegate = null;

        /// <summary>
        /// Delegate for update ui for parent
        /// </summary>
        private EthernetTools.Utils.Delegates.VoidDelegate UpdateParentUI = null;
         

        /// <summary>
        ///  Ethernet packet type
        /// </summary>
        public static readonly byte[] EthType = new byte[2] { (byte)0x88, (byte)0x63 };

        private PPPoETagsContainer pppoeTags = new PPPoETagsContainer();

        private String2ByteDataBinding SourceMacBinding = new String2ByteDataBinding();

        private String2ByteDataBinding DestMacBinding = new String2ByteDataBinding();

        private String2ByteDataBinding SessionIdBinding = new String2ByteDataBinding();

        private Control PlayloadLength;

        private String2ByteDataBinding ACName = new String2ByteDataBinding();

        private String2ByteDataBinding HostUniqueId = new String2ByteDataBinding();
        
        private ListStringToBytes TypeContainer = new ListStringToBytes();

        private bool AlreadyUpdatingGui = false;

        public class PPPoEType : Object
        {
            public int Value 
            {
                get ; 
                private set;
            }

            public const int PADS = 0x1165;
            public const int PADO = 0x1107;
            public const int PADI = 0x1109;
            public const int PADR = 0x1119;
            public const int PADT = 0x11a7;

            public PPPoEType(int val)
            {
                Value = val;
            }

            public override string ToString()
            {
                switch (Value)
                {
                    case PADS:
                        return "PADS";
                    case PADO:
                        return "PADO";
                    case PADI:
                        return "PADI";
                    case PADR:
                        return "PADR";
                    case PADT:
                        return "PADT";

                }
                return "Not PPPoE Type:" + Value;
            }
            
            public byte[] ToByteArray()
            {
                 return new byte[] { (byte)((int)Value >> 8), (byte)((int)Value & 0x00FF) };
            }

            public int GetComboBoxIndex()
            {

                switch (Value)
                {
                    case PADI:
                        return 0;
                    case PADO:
                        return 1;
                    case PADR:
                        return 2;
                    case PADS:
                        return 3;
                    case PADT:
                        return 4;

                }
                return -1;
            }

            public static PPPoEType Parse(byte[] val)
            {
                if (val.Length < 2)
                    return null;

                return new PPPoEType((val[0] << 8) | val[1]);
            }
        }

        #endregion 
        
        #region Properties

        /// <summary>
        /// Source mac address
        /// </summary>
        public byte[] Source
        {
            get { return SourceMacBinding.RawData; }
        }

        /// <summary>
        /// Source mac address presented in string
        /// </summary>
        public string sSource
        {
            get { return SourceMacBinding.StringValue; }
        }

        /// <summary>
        /// Destination mac address
        /// </summary>
        public byte[] Destination
        {
            get { return DestMacBinding.RawData; }
        }

        /// <summary>
        /// Destination mac address  presented in string
        /// </summary>
        public string sDestination
        {
            get { return DestMacBinding.StringValue; }
        }

        /// <summary>
        /// PPPoE packet type
        /// </summary>
        public byte[] Type
        {
            get
            {
                PPPoEType pt = (PPPoEType)TypeContainer.SelectedItem;
                if (pt != null)
                {
                    return pt.ToByteArray();
                }

                return null;
            }
            private set
            {
            }
        }

        /// <summary>
        ///  same as Type, only present like int
        /// </summary>
        public int iType
        {
            get
            {
                PPPoEType pt = (PPPoEType)TypeContainer.SelectedItem;
                if (pt != null)
                {
                    return pt.Value;
                }

                return 0;
            }
        }

        /// <summary>
        /// PPPoE session id
        /// </summary>
        public byte[] SessionId
        {
            get { return SessionIdBinding.RawData; }
        }

        /// <summary>
        /// PPPoE tags
        /// </summary>
        public PPPoETagsContainer Tags
        {
            get { return pppoeTags; }
        }

        /// <summary>
        /// By default it will be 0x11
        /// </summary>
        public byte[] Version
        {
            get;
            private set;
        }

        public byte[] Unknown
        {
            get;
            private set;
        }
        #endregion
        
        #region Constructors And Destructors

        public PppoeProtocolPacket(EthernetTools.Utils.Delegates.HandleLogDelegate logWriteDelegate, EthernetTools.Utils.Delegates.VoidDelegate updateParentUI)
        {
            this.LogWriteDelegate = logWriteDelegate;
            this.UpdateParentUI = updateParentUI;
        }
       
        private PppoeProtocolPacket(byte[] rawData)
        {
            this.DestMacBinding.Bind();
            this.SourceMacBinding.Bind();
            this.SessionIdBinding.Bind();

            this.DestMacBinding.RawData = ByteUtils.RemoveBytes(6, ref rawData);
            this.SourceMacBinding.RawData = ByteUtils.RemoveBytes(6, ref rawData);
           
            // compare EthType
            byte[] tempBytes = ByteUtils.RemoveBytes(2, ref rawData);
            if (tempBytes[0] != PppoeProtocolPacket.EthType[0]
                || tempBytes[1] != PppoeProtocolPacket.EthType[1])
            {
                if (LogWriteDelegate != null)
                    LogWriteDelegate("Not pppoe", Options.Global.LogLevel.Warning);
                return;
            }

            // get Version Type
            Version = new byte[] {rawData[0]};

            // get Code Type
            this.TypeContainer.Bind(null, null,
                new List<object> { PPPoEType.Parse(ByteUtils.RemoveBytes(2, ref rawData)) } );

            this.SessionIdBinding.RawData = ByteUtils.RemoveBytes(2, ref rawData);

            tempBytes = ByteUtils.RemoveBytes(2, ref rawData);
            
            uint playLoadLength = (uint)((tempBytes[0] << 8) | tempBytes[1]);

            this.pppoeTags = PPPoETagsContainer.ParseFromBytes(ByteUtils.RemoveBytes(playLoadLength, ref rawData));

            this.Unknown = rawData;
        }

        #endregion
        
        #region Public Methods

        /// <summary>
        /// Bind internal byte data to User controls
        /// </summary>
        /// <param name="srcMac">TextBox for source mac</param>
        /// <param name="dstMac">TextBox for destination mac</param>
        /// <param name="playloadLength">TextBox for playloadLength</param>
        /// <param name="acName">TextBox for AC name</param>
        /// <param name="hostUniqueId">TextBox for Host Unique Id</param>
        /// <param name="sessionId">TextBox for session ID</param>
        /// <param name="ppoeTypeContainer">Control (ComboBox) for ppoe type</param>
        public void InitUiBindings(Control srcMac,
            Control dstMac, Control playloadLength, Control acName, Control hostUniqueId, Control sessionId , ComboBox ppoeTypeContainer,
            TreeView pppoetagsView,
            Control additionalSrc, Control additionalDst)
        {
            this.DestMacBinding.Bind(dstMac, UpdateUI, ByteParseStringMethod.String2ByteConverting, 6, additionalDst);
            this.SourceMacBinding.Bind(srcMac, UpdateUI, ByteParseStringMethod.String2ByteConverting, 6, additionalSrc);

            this.SessionIdBinding.Bind(sessionId, UpdateUI, ByteParseStringMethod.String2ByteConverting, 2);

            this.ACName.Bind(acName, UpdateUI, ByteParseStringMethod.String2ByteDirectly);
            
            this.HostUniqueId.Bind(hostUniqueId, UpdateUI);
           
            List<Object> TempArr = new List<Object>() {
                new PPPoEType(PPPoEType.PADI), 
                new PPPoEType(PPPoEType.PADO), 
                new PPPoEType(PPPoEType.PADR),
                new PPPoEType(PPPoEType.PADS),  
                new PPPoEType(PPPoEType.PADT)  };

            TypeContainer.Bind(ppoeTypeContainer, UpdateUI, TempArr);
                        
            this.PlayloadLength = playloadLength;

            if (this.Type != null)
            {
                PPPoEType ppt = PPPoEType.Parse(this.Type);
                this.pppoeTags.Init(pppoetagsView, ppt.Value);

                TypeContainer.Select(ppt.GetComboBoxIndex());
            }
            else
            {
                this.pppoeTags.Init(pppoetagsView);
            }
            
            this.HostUniqueId.Verify();
            this.ACName.Verify();
        }

        public void UpdateUI(Object sender, EventArgs e)
        {
            if (AlreadyUpdatingGui)
            {
                return;
            }

            if (UpdateParentUI != null)
                UpdateParentUI();

            AlreadyUpdatingGui = true;

            PPPoEType pt = (PPPoEType)TypeContainer.SelectedItem;
            if (pt != null)
            {
                pppoeTags.SetPPPoeTags(pt.Value, HostUniqueId, ACName, null);
                PlayloadLength.Text = ByteUtils.ByteArrToString(pppoeTags.GetPlayLoadLength());
            }
            SetCurrentPPPoESessID();

            AlreadyUpdatingGui = false;
        }
        
        /// <summary>
        /// Call when binded to ui packet not need anymore
        /// </summary>
        public void Unbind()
        {
            this.DestMacBinding.UnBind();

            this.SourceMacBinding.UnBind();

            this.SessionIdBinding.UnBind();

            this.ACName.UnBind();

            this.HostUniqueId.UnBind();

            TypeContainer.UnBind();

            this.pppoeTags.Release();
            
        }
        #endregion
        
        #region Methods
        
        /// <summary>
        /// Get Bytes for current PPPoE type - PAD(*) to 2 bytes
        /// </summary>
        /// <returns></returns>
        private byte[] GetPPPoETypeBytes()
        {
            PPPoEType pt = (PPPoEType)TypeContainer.SelectedItem;
            if (pt != null)
            {
                return pt.ToByteArray();
            }

            return null;
        }

        /// <summary>
        /// Get packet session Id (Parse from string) 
        /// </summary>
        /// <returns></returns>
        private void SetCurrentPPPoESessID()
        {
            PPPoEType pt = (PPPoEType)TypeContainer.SelectedItem;
            if (pt != null)
            {
                switch (pt.Value)
                {
                    case PPPoEType.PADS:
                    case PPPoEType.PADT:
                        {
                            SessionIdBinding.StringContainer.Enabled = true;
                            SessionIdBinding.Verify();
                            break;
                        }
                    case PPPoEType.PADO:
                    case PPPoEType.PADR:
                        {
                            SessionIdBinding.StringContainer.Enabled = false;
                            SessionIdBinding.StringContainer.BackColor = System.Drawing.Color.LightGreen;
                            SessionIdBinding.Verify();
                            break;
                        }
                    case PPPoEType.PADI:
                        {
                            if (DestMacBinding.StringContainer.Text == "")
                            {
                                DestMacBinding.StringContainer.Text = "ff:ff:ff:ff:ff:ff";
                            }
                            SessionIdBinding.StringContainer.Enabled = false;
                            SessionIdBinding.StringContainer.BackColor = System.Drawing.Color.LightGreen;
                            SessionIdBinding.Verify();
                            break;
                        }
                }
            }
        }
        

        #endregion
        
        #region Iterface implementation

        /// <summary>
        /// Return full ethernet frame packet (if all params presented correctly)
        /// </summary>
        /// <returns></returns>
        public byte[] ToBytesArray()
        {
            PPPoEType pt = (PPPoEType)TypeContainer.SelectedItem;

            if (DestMacBinding.IsCorrect
                && SourceMacBinding.IsCorrect
                && SessionIdBinding.IsCorrect
                && ACName.IsCorrect
                && HostUniqueId.IsCorrect
                && pt != null)
            {
                pppoeTags.SetPPPoeTags(pt.Value, HostUniqueId, ACName, null);

                byte[] packet = new byte[0];
                ByteUtils.AppendBytes(ref packet, DestMacBinding.RawData);
                ByteUtils.AppendBytes(ref packet, SourceMacBinding.RawData);
                ByteUtils.AppendBytes(ref packet, EthType);
                ByteUtils.AppendBytes(ref packet, GetPPPoETypeBytes());
                if (SessionIdBinding.StringContainer.Enabled)
                {
                    ByteUtils.AppendBytes(ref packet, SessionIdBinding.RawData);
                }
                else
                {
                    ByteUtils.AppendBytes(ref packet, new byte[] { 0x00, 0x00 });
                }
                ByteUtils.AppendBytes(ref packet, pppoeTags.GetPlayLoadLength());
                ByteUtils.AppendBytes(ref packet, pppoeTags.ToBytes());

                // TODO: Adding checksum
                return packet;
            }
            else
            {
                LogWriteDelegate("Cant send packet because:", Options.Global.LogLevel.Error);
                if (!DestMacBinding.IsCorrect )
                {
                    LogWriteDelegate("Destination mac incorrect!", Options.Global.LogLevel.Error);
                }
                if (!SourceMacBinding.IsCorrect)
                {
                    LogWriteDelegate("Source mac incorrect!", Options.Global.LogLevel.Error);
                }
                if (!SessionIdBinding.IsCorrect)
                {
                    LogWriteDelegate("Session id incorrect!", Options.Global.LogLevel.Error);
                }
                if (!ACName.IsCorrect)
                {
                    LogWriteDelegate("AC Name incorrect!", Options.Global.LogLevel.Error);
                }
                if (!HostUniqueId.IsCorrect)
                {
                    LogWriteDelegate("Host unique id incorrect!", Options.Global.LogLevel.Error);
                }
                if (pt == null)
                {
                    LogWriteDelegate("PPPoE packet type not chosen", Options.Global.LogLevel.Error);
                }
            }

            return null;
        }

        #endregion

        #region PUblic static 
        
        public static PppoeProtocolPacket ParseFromBytes(byte[] RawData)
        {
            // sMAC-dMAC-0x8864-PPPOETYPE-ssesid-plllen
            if (RawData.Length < 18)
            {
                return null;
            }

            return new PppoeProtocolPacket(RawData);
        }

        public static byte[] GetPADIPacket(byte[] srcMAC)
        {
            if (srcMAC.Length < 6)
                return null;
            
            byte[] packet = new byte[0];
            
            ByteUtils.AppendBytes(ref packet, new byte[] {0xFF,0xFF,0xFF,0xFF,0xFF,0xFF});
            ByteUtils.AppendBytes(ref packet, srcMAC);
            ByteUtils.AppendBytes(ref packet, EthType);
            ByteUtils.AppendBytes(ref packet, new PPPoEType(PPPoEType.PADI).ToByteArray());
            ByteUtils.AppendBytes(ref packet, new byte[] {0x00,0x00,0x00,0x14}); // ssid + pllen


            ByteUtils.AppendBytes(ref packet, new byte[]
            { (byte)((int)PPPoETag.TagType.ServiceName >> 8), 
                (byte)((int)PPPoETag.TagType.ServiceName & 0x00FF), 
                0x00, 0x00 });

            ByteUtils.AppendBytes(ref packet, new byte[]
            { (byte)((int)PPPoETag.TagType.HostUniq >> 8), 
                (byte)((int)PPPoETag.TagType.HostUniq & 0x00FF), 
                0x00, 0x0c });
            // host uid (can be any)
            ByteUtils.AppendBytes(ref packet, ByteUtils.ParseBytes("ca:01:00:00:00:00:00:00:a2:02:00:00"));

            return packet;
        }

        public static byte[] GetPADTPacket(byte[] dstMAC, byte[] srcMAC, byte[] sessid)
        {
            if (srcMAC.Length < 6)
                return null;
            if (dstMAC.Length < 6)
                return null;
            if (sessid.Length < 2)
                return null;

            byte[] packet = new byte[0];

            ByteUtils.AppendBytes(ref packet, dstMAC);
            ByteUtils.AppendBytes(ref packet, srcMAC);
            ByteUtils.AppendBytes(ref packet, EthType);
            ByteUtils.AppendBytes(ref packet, new PPPoEType(PPPoEType.PADT).ToByteArray());
            ByteUtils.AppendBytes(ref packet, sessid);
            //pllen
            ByteUtils.AppendBytes(ref packet, new byte[] { 0x00, 0x00 });

            return packet;
        }

        public static byte[] GetPADOPacket(byte[] dstMAC, byte[] srcMAC, byte[] hostUniq, byte[] AcName, byte[] unknown = null)
        {
            if (srcMAC.Length < 6)
                return null;
            if (dstMAC.Length < 6)
                return null;
            if (hostUniq == null)
                return null;

            byte[] packet = new byte[0];

            ByteUtils.AppendBytes(ref packet, dstMAC);
            ByteUtils.AppendBytes(ref packet, srcMAC);
            ByteUtils.AppendBytes(ref packet, EthType);
            ByteUtils.AppendBytes(ref packet, new PPPoEType(PPPoEType.PADO).ToByteArray());
            ByteUtils.AppendBytes(ref packet, new byte[] { 0x00, 0x00 });
            //pllen
            ByteUtils.AppendBytes(ref packet, new byte[] { 0x00, (byte)(hostUniq.Length + 4 + AcName.Length + 4 + 4) });

            ByteUtils.AppendBytes(ref packet, new byte[]
            { (byte)((int)PPPoETag.TagType.HostUniq >> 8), 
                (byte)((int)PPPoETag.TagType.HostUniq & 0x00FF), 
                0x00, (byte)(hostUniq.Length) });
            // host uid 
            ByteUtils.AppendBytes(ref packet, hostUniq);

            ByteUtils.AppendBytes(ref packet, new byte[]
            { (byte)((int)PPPoETag.TagType.ACName >> 8), 
                (byte)((int)PPPoETag.TagType.ACName & 0x00FF), 
                0x00, (byte)(AcName.Length) });
            // ac name
            ByteUtils.AppendBytes(ref packet, AcName);

            ByteUtils.AppendBytes(ref packet, new byte[]
            { (byte)((int)PPPoETag.TagType.ServiceName >> 8), 
                (byte)((int)PPPoETag.TagType.ServiceName & 0x00FF), 
                0x00, 0x00 });

            // chsum and other.. at now getting troubles with that
            if (unknown != null)
            {
                ByteUtils.AppendBytes(ref packet, unknown);
            }

            return packet;
        }
      
        public static byte[] GetPADOPacket(byte[] dstMAC, byte[] srcMAC, byte[] hostUniq, byte[] AcName, byte[] serviceName, byte[] unknown = null)
        {
            if (srcMAC.Length < 6)
                return null;
            if (dstMAC.Length < 6)
                return null;
            if (hostUniq == null)
                return null;

            byte[] packet = new byte[0];

            ByteUtils.AppendBytes(ref packet, dstMAC);
            ByteUtils.AppendBytes(ref packet, srcMAC);
            ByteUtils.AppendBytes(ref packet, EthType);
            ByteUtils.AppendBytes(ref packet, new PPPoEType(PPPoEType.PADO).ToByteArray());
            ByteUtils.AppendBytes(ref packet, new byte[] { 0x00, 0x00 });
            //pllen
            ByteUtils.AppendBytes(ref packet, new byte[] { 0x00, 
                (byte)( (hostUniq.Length + 4) + (AcName.Length + 4) + (4 + serviceName.Length)  ) });

            ByteUtils.AppendBytes(ref packet, new byte[]
            { (byte)((int)PPPoETag.TagType.ServiceName >> 8), 
                (byte)((int)PPPoETag.TagType.ServiceName & 0x00FF), 
                0x00, (byte)(serviceName.Length) });
            // service name
            ByteUtils.AppendBytes(ref packet, serviceName);

            ByteUtils.AppendBytes(ref packet, new byte[]
            { (byte)((int)PPPoETag.TagType.HostUniq >> 8), 
                (byte)((int)PPPoETag.TagType.HostUniq & 0x00FF), 
                0x00, (byte)(hostUniq.Length) });
            // host uid 
            ByteUtils.AppendBytes(ref packet, hostUniq);

            ByteUtils.AppendBytes(ref packet, new byte[]
            { (byte)((int)PPPoETag.TagType.ACName >> 8), 
                (byte)((int)PPPoETag.TagType.ACName & 0x00FF), 
                0x00, (byte)(AcName.Length) });
            // ac name
            ByteUtils.AppendBytes(ref packet, AcName);

            // chsum and other.. at now getting troubles with that
            if (unknown != null)
            {
                ByteUtils.AppendBytes(ref packet, unknown);
            }

            return packet;
        }
        #endregion

    }
}
